#!/usr/bin/python
"""blaj v0.1 proof of concept - Author: Jim Palmer - MIT license"""
from datetime import datetime
from email import Utils
import glob
import logging
import logging.handlers
import md5
import os
import re
import smtplib
import sys
import simplejson as json
import traceback

class Blaj:
	
	password = 'password'
	blajurl = 'http://www.blaj.blaj/blaj.cgi?'
	email = 'admin@blaj.blaj'
	replyto = '"blaj" <admin@blaj.blaj>'
	postdir = '../posts/'
	blajdir = ''
	tdir = 'assets/templates/'
	dateformat = '%m/%d/%Y'
	commentdateformat = '%m/%d/%Y %I:%M%p'
	urldateformat = '%Y/%m/%d/'
	gravicon = ''
	
	webmethods = []
	postkeys = 'title,category,keywords,body,excerpt,posts,date'
	defaultpost = dict.fromkeys(postkeys.split(','), '')
	opentemplates = {}
	auth = ''
	
	def __init__(self, auth):
		Blaj.auth = auth
	
	class AuthException(Exception):
		def __init__(self, message):
			self.message = message
		def __str__(self):
			return str(self.message)
	
	def Webmethod(func):
		def decorator(*a, **k):
			# construct friendly url from title
			if 'title' in a[1]: 
				a[1]['url'] = re.sub('[^0-9a-z ]', '', a[1]['title'].lower())
				a[1]['url'] = re.sub('[ ]', '-', a[1]['url'])
			return func(*a, **k)
		setattr(decorator, 'Webmethod', True)
		return decorator

	def Authenticate(func):
		def decorator(*a, **k):
			if Blaj.auth != md5.new(Blaj.password).hexdigest():
				raise Blaj.AuthException('authentication failed')
			return func(*a, **k)
		setattr(decorator, 'Webmethod', True)
		return decorator
	
	@Webmethod
	@Authenticate
	def preview(self, postdata):
		"""preview post - authenticated"""
		return self.rendert('blaj', postdata)
	
	@Webmethod
	@Authenticate
	def previewexcerpt(self, postdata):
		"""preview post excerpt - authenticated"""
		archive = {'archive': [{'home.post': postdata}]}
		return self.rendert('home', dict(archive, **postdata))

	@Webmethod
	@Authenticate
	def newpost(self, postdata=defaultpost):
		"""new post form - authenticated"""
		return self.rendert('newpost', postdata)
	
	@Webmethod
	@Authenticate
	def addpost(self, postdata):
		"""add post - authenticated"""
		if not os.path.exists(self.postdir + postdata['url']):
			os.makedirs(self.postdir + postdata['url'])
		json.dump(postdata, open(self.postdir + postdata['url'] + '/post', 'w'))
		return self.processposts()
	
	@Webmethod
	@Authenticate
	def processallcomments(self, postdata):
		"""process all comments - authenticated"""
		posts = ''
		for postfile in glob.glob(self.postdir + '*/post'):
			self.processcomments(postfile.split('/')[-2])
			posts += postfile.split('/')[-2] + '<br>'
		return posts
	
	@Webmethod
	def addcomment(self, postdata=defaultpost):
		"""add new comment"""
		post = postdata.get('post','');
		if post and not os.path.exists(self.postdir + post):
			return 'ERROR: invalid post'
		posttime = datetime.now().strftime('%s')
		json.dump(postdata, open(self.postdir + post + '/comment.' + posttime + '.moderate', 'w'))
		postdata['subject'] = 'moderate comment "' + post + '"'
		passhash = md5.new(self.password).hexdigest()
		postdata['spamlink'] = self.blajurl + 'spam-' + posttime + '-' + passhash
		postdata['approvelink'] = self.blajurl + 'approve-' + posttime + '-' + passhash
		self.sendmail(postdata, self.tdir + 'moderatecomment')
		return 'SUCCESS'

	@Webmethod
	@Authenticate
	def approve(self, timestamp, auth):
		"""new post form - authenticated"""
		post, newcfile, msuff = '', '', '.moderate'
		for comment in glob.glob(self.postdir + '*/comment.' + timestamp + msuff):
			newcfile = comment[:-len(msuff)] + '.approved'
			os.rename(comment, newcfile)
			post = comment.split('/')[-2]
		if post:
			return 'SUCCESS' + str(self.processcomments(post, newcfile))
		else:
			return 'ERROR: comment does not exist'
	
	@Webmethod
	def unsubscribe(self, commentstamp):
		"""unsubscribe from update notifications"""
		postdir, email = '', ''
		commentstamp = long(commentstamp)
		for cfile in glob.glob(self.postdir + '*/comment.' + commentstamp + '*'):
			cfh = open(cfile)
			comment = json.loads(cfh.read())
			email = comment['email']
			currpd = cfile.split('/')[-2]
			cfh.close()
		if len(currpd) and len(email):
			for ocfile in glob.glob(self.postdir + currpd + '/*'):
				readc = open(cfile)
				rcomment = json.load(readc)
				readc.close()
				if rcomment['email'] == email:
					rcomment['update'] = '0'
					cfh = open(cfile, 'w')
					json.dump(rcomment, cfh)
					cfh.close()
		return 'SUCCESS'
	
	def rendert(self, tf, data={}):
		"""single-level recursive render template utility function"""
		for sub in data:
			if isinstance(data[sub], [].__class__):
				rectemplate = ''
				for subdata in data[sub]:
					for subtf in subdata:
						rectemplate += self.rendert(subtf, subdata[subtf])
				data[sub] = rectemplate
		if self.tdir + tf not in self.opentemplates:
			tfile = open(self.tdir + tf)
			self.opentemplates[self.tdir + tf] = tfile.read()
			tfile.close()
		return self.opentemplates[self.tdir + tf] % data

	def saveindex(self, template, data, postpath, fname='/index.html'):
		"""save processed template file"""
		if len(postpath):
			if not os.path.exists(postpath): 
				os.makedirs(postpath)
		else:
			postpath = '.'
		ifile = open(postpath + fname, 'w')
		ifile.write(self.rendert(template, data).encode('utf-8'))
		ifile.close()
	
	def processposts(self):
		"""process posts"""
		catarchive, datearchive, categories = {}, {}, {}
		posts, homeposts, rssposts, parch = [], [], [], []
		for postfile in glob.glob(self.postdir + '*/post'):
			postdata = json.load(open(postfile))
			postdata['datetime'] = datetime(*Utils.parsedate(postdata['datetime'])[:6])
			postdata['date'] = postdata['datetime'].strftime(self.dateformat)
			postdata['url'] = '/' + self.blajdir + postdata['datetime'].strftime(self.urldateformat) + postdata['url']
			posts.append(postdata)
		posts.sort(lambda x, y: cmp(y['datetime'], x['datetime']))
		for post in posts:
			self.saveindex('blaj', post, post['url'][1:])
			arch = {'archive.post': post}
			postsarch = {'posts.post': post}
			if post['category'] and post['category'] in catarchive: 
				catarchive[post['category']].append(arch)
				categories[post['category']].append(postsarch)
			elif post['category']:
				catarchive[post['category']] = [arch]
				categories[post['category']] = [postsarch]
			dateparts = post['datetime'].strftime(self.urldateformat).split('/')
			for datepart in ['/'.join(dateparts[:i]) for i in range(1, len(dateparts))]:
				if datepart in datearchive:
					datearchive[datepart].append(arch)
				else:
					datearchive[datepart] = [arch]
			homeposts.append({'home.post': post})
			rssposts.append({'rss.post': post})
		
		self.saveindex('home', dict(self.defaultpost, **{'archive':homeposts}), self.blajdir)
		self.saveindex('rss', dict({'posts':rssposts}, **post), self.blajdir, '/rss.xml')
		for cat in catarchive.keys():
			self.saveindex('archive', dict(self.defaultpost, 
											**{'category':cat, 'archive':catarchive[cat]}),
											self.blajdir + cat)
			parch.append(self.rendert('posts.category',
							{'category':cat, 
							 'posts':categories[cat],
							 'url':'/' + self.blajdir + cat,
							 'postcount':len(catarchive[cat])}))
		for year in datearchive.keys():
			self.saveindex('archive', dict(self.defaultpost,
											**{'category':year, 'archive':datearchive[year]}),
											self.blajdir + year)
		# render posts page off categories list
		open('posts.html', 'w').write(unicode('').join(parch).encode('utf-8'))
		# output the rendered debug template
		return self.rendert('debug', {'posts':str([p['title'] for p in posts]), 
									 	'categories':str(categories.keys()), 
									 	'datearchive':str(datearchive.keys())})

	def processcomments(self, post, newcfile=''):
		"""process all comments for a given post"""
		comments, notify = [], {}
		for cfile in glob.glob(self.postdir + post + '/comment.*.approved'):
			cobj = json.load(open(cfile))
			grav = md5.new(cobj['email']).hexdigest()
			cobj['gravatar'] = 'http://www.gravatar.com/avatar/' + grav + '?s=50&d=' + self.gravicon
			cobj['hash'] = cfile.split('.')[-2]
			cobj['datetimeobj'] = datetime.fromtimestamp(float(cobj['hash']))
			cobj['datetime'] = cobj['datetimeobj'].strftime(self.commentdateformat)
			if 'http' in cobj['url']:
				cobj['name'] = '<a href="' + cobj['url'] + '">' + cobj['name'] + '</a>'
			cobj['message'] = re.sub('(?<![>"])(http://[^ <"]+)', '<a href="\\1">\\1</a>', cobj['message'])
			nlre = re.compile('(?<!<br />)(?<!<br/>)(?<!<br>)\n(?!<br)(?![^<]*</pre>)', re.I)			
			cobj['message'] = re.sub(nlre, '\n<br />', cobj['message'])
			comments.append(cobj)
			if cobj.get('update', '0') == '1' and cobj.get('email', ''):
				notify[cobj.get('email', '')] = cfile.split('.')[-2]
		comments.sort(lambda x, y: cmp(x['datetimeobj'], y['datetimeobj']))
		postdata = json.load(open(self.postdir + post + '/post'))
		postdata['datetime'] = datetime(*Utils.parsedate(postdata['datetime'])[:6])
		postdata['url'] = self.blajdir + postdata['datetime'].strftime(self.urldateformat) + postdata['url']
		if len(comments):
			open(postdata['url'] + '/comments.html', 'w').write(
				''.join([self.rendert('comment', c) for c in comments]).encode('utf-8'))
		if len(newcfile):
			for email in notify:
				commdata = json.load(open(newcfile))
				commdata['subject'] = 'new comment on "' + post + '"'
				commdata['to'] = email
				commdata['title'] = postdata['title']
				commdata['spamlink'] = self.blajurl + 'unsubscribe-' + notify[email]
				self.sendmail(commdata, self.tdir + 'commentnotify')
		return notify

	def sendmail(self, postdata, template):
		postdata['to'] = self.email
		postdata['from'] = self.replyto
		msg = open(template).read() % postdata
		s = smtplib.SMTP('localhost')
		s.sendmail(postdata['from'], [postdata['to']], msg)
		s.quit()

def BlajCGI():
	"""Blaj static CGI handler"""
	cookies = os.environ.get('HTTP_COOKIE', '').split(';')
	auth = dict([c.strip().split('=') for c in cookies if 'auth' in c]).get('auth', '')
	blaj = Blaj(auth)
	reqmethod = os.environ.get('REQUEST_METHOD', '')
	querystr = os.environ.get('QUERY_STRING', '')
	try:
		if reqmethod == 'POST':
			postdata = json.loads(sys.stdin.read(int(os.environ.get('CONTENT_LENGTH', '0'))))
			if getattr(getattr(blaj, querystr), 'Webmethod', False):
				print getattr(blaj, querystr)(postdata)
			else:
				print 'ERROR: invalid request'
		elif reqmethod == 'GET':
			actions = querystr.split('-')
			if hasattr(blaj, actions[0]) and getattr(getattr(blaj, actions[0]), 'Webmethod', False):
				blaj = Blaj(actions[-1])
				print getattr(blaj, actions[0])(*actions[1:])
			else:
				postdata = dict(blaj.defaultpost)
				postdata['date'] = datetime.now().strftime(blaj.dateformat)
				posts = [post.split('/')[-2] for post in glob.glob(blaj.postdir + '*/post')]
				posts.sort()
				postdata['posts'] = json.dumps([''] + posts)
				currpost = {}
				postfile = blaj.postdir + querystr + '/post'
				if os.path.exists(postfile):
					pf = open(postfile)
					pfs = re.sub('&', '&amp;', pf.read())
					currpost = json.loads(pfs)
					pf.close()
				print blaj.newpost(dict(postdata, **currpost))
	except Blaj.AuthException,e:
		print open(Blaj.tdir + 'auth').read()
	except:
		log = logging.getLogger('')
		log.setLevel(logging.ERROR)
		log.addHandler(logging.handlers.RotatingFileHandler(
						Blaj.postdir + 'blaj.log', maxBytes=2000000, backupCount=3))
		log.error(str(datetime.now()) + '\n' + traceback.format_exc())
		print 'ERROR: unexpected error occurred'

if __name__ == '__main__':
	print 'Content-Type: text/html\n\n'
	BlajCGI()
